package com.lt.lumentracks.service.Impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.json.JSONUtil;
import com.aliyuncs.exceptions.ClientException;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lt.lumentracks.constant.ElasticSearchConstant;
import com.lt.lumentracks.constant.RedisConstant;
import com.lt.lumentracks.context.BaseContext;
import com.lt.lumentracks.entity.dto.post.PostAddDTO;
import com.lt.lumentracks.entity.dto.post.PostDTO;
import com.lt.lumentracks.entity.dto.post.PostReadDTO;
import com.lt.lumentracks.entity.dto.post.PostUpdateDTO;
import com.lt.lumentracks.entity.po.*;
import com.lt.lumentracks.entity.po.elasticsearch.PostDoc;
import com.lt.lumentracks.entity.vo.common.PageVO;
import com.lt.lumentracks.entity.vo.common.Result;
import com.lt.lumentracks.entity.vo.post.PostVO_list;
import com.lt.lumentracks.entity.vo.post.PostVO_raw;
import com.lt.lumentracks.entity.vo.post.PostVO_show;
import com.lt.lumentracks.entity.vo.user.UserVO_post;
import com.lt.lumentracks.mapper.CommentMapper;
import com.lt.lumentracks.mapper.PostMapper;
import com.lt.lumentracks.service.*;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lt.lumentracks.utils.AliOSSUtils;
import com.lt.lumentracks.utils.CommonUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.xcontent.XContentType;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.BeanUtils;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.BoundZSetOperations;
import org.springframework.data.redis.core.DefaultTypedTuple;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * <p>
 * 帖子主表 服务实现类
 * </p>
 *
 * @author swl
 * @since 2025-07-11
 */
@Slf4j
@RequiredArgsConstructor
@Service
public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements IPostService {

    private final PostMapper postMapper;
    private final IUserService iUserService;
    private final IPostImageService iPostImageService;
    private final IPostTagService iPostTagService;
    private final IPostTopicService iPostTopicService;
    private final ITempFileService tempFileService;
    private final CommentMapper commentMapper;

    private final IPostDocService iPostDocService; // es--帖子索引文档服务类
    private final RestHighLevelClient esClient;
    private final AliOSSUtils aliOSSUtils;
    private final RedisTemplate<String, String> redisTemplate_String; // redis操作模版--字符串类型

    private final int MAX_LIKE_COUNT = 100;
    private final int MAX_COLLECT_COUNT = 500;
    private static final int POST_HOT_NUM = 1000; // 热帖数量


    /** 添加帖子
     * @param addDTO 添加的帖子信息
     * @return 添加的帖子id
     */
    @Transactional // 开启事务
    @Override
    public Result<Integer> addPost(PostAddDTO addDTO) {
        Post post = new Post();
        post.setCreateTime(LocalDateTime.now());
        BeanUtils.copyProperties(addDTO, post);

        // 1.post--帖子表新增
        this.save(post);

        // 2.post_image--帖子图片关联表表新增（注意要额外添加图片的索引）
        if(!addDTO.getImageUrlList().isEmpty()){
            postMapper.insert_PostImage(post.getId(), addDTO.getImageUrlList());
        }

        // 3.post_tag--帖子标签关联表新增
        if(!addDTO.getTagIdList().isEmpty()){
            postMapper.insert_PostTag(post.getId(), addDTO.getTagIdList());
        }

        // 4.post_topic--帖子话题关联表新增
        if(!addDTO.getTopicIdList().isEmpty()){
            postMapper.insert_PostTopic(post.getId(), addDTO.getTopicIdList());
        }

        // 5.用户发布帖子的数量加1
        iUserService.update(new UpdateWrapper<User>()
                .eq("id", post.getUserId())
                .setSql("posts_count = posts_count + 1"));

        // 6.将帖子信息保存到es中，这一步在后台管理系统中完成，审核通过就存入es中
        // TODO: 开发环境，目前先直接存入es，便于测试
        iPostDocService.addPostDoc(post);

        // 7.将在临时文件表里面的图片地址信息删除
        tempFileService.removeBatchByIds(addDTO.getImageUrlList());

        // 返回添加的id（主键返回）
        return Result.success(post.getId());
    }


    /**
     * <h2>批量操作</h2>
     * <p>请求对象--BulkRequest</p>
     * <p>调用方法--Client.bulk()</p>
     */
    // 批量添加,用于同步mysql数据库
    // 往ES中同步帖子信息
    public void BulkDoc_save() {

        // 同步之前先删除该索引原本的文档信息
        iPostDocService.deleteAllDocs(ElasticSearchConstant.POST_INDEX_NAME);

        int pageNo = 1, pageSize = 500;
        while (true) {
            // 1、准备数据
            Page<Post> pageList = this.lambdaQuery().page(Page.of(pageNo, pageSize));
            List<Post> records = pageList.getRecords();
            if(records == null || records.isEmpty()){
                return; // 批量操作结束，退出循环
            }

            // 2、准备Request
            BulkRequest req = new BulkRequest();

            // 3、准备请求参数
            for (Post p : records) {
                req.add(new IndexRequest(ElasticSearchConstant.POST_INDEX_NAME)
                    .id(p.getId().toString()) // 文档的id（与对象的id一致就行）
                    .source(JSONUtil.toJsonStr(BeanUtil.copyProperties(p, PostDoc.class)), XContentType.JSON)); // 具体内容（把要传的对象转成JSON格式传过去）
            }

            // 4、发送请求
            try {
                esClient.bulk(req, RequestOptions.DEFAULT);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }

            // 5、翻页
            pageNo ++;
        }
    }


    // 删除帖子
    @Transactional
    @Override
    @CacheEvict(value = RedisConstant.POST_RAW_DETAIL_KEY, key = "#postId") // 删除缓存
    public Result<String> delPostById(Integer postId) {

        // 1.帖子图片关联表删除
        List<PostImage> imgList = iPostImageService.findByPostId(postId);
        if (!imgList.isEmpty()) {
            for (PostImage pi : imgList) {
                // 1.1.删除云存储中的该图片
                try {
                    aliOSSUtils.deleteObject(pi.getImageUrl());
                } catch (ClientException e) {
                    throw new RuntimeException(e);
                }

                // 1.2.删除帖子图片关联表中的相关信息
                iPostImageService.removeById(pi.getId());
            }
        }

        // 2.帖子标签关联表删除
        QueryWrapper<PostTag> tagQw = new QueryWrapper<>();
        tagQw.eq("post_id", postId);
        iPostTagService.remove(tagQw);

        // 3.帖子话题关联表删除
        QueryWrapper<PostTopic> topicQw = new QueryWrapper<>();
        topicQw.eq("post_id", postId);
        iPostTopicService.remove(topicQw);

        // 4.该用户发布帖子的数量减1
        Post post = this.getOne(new LambdaQueryWrapper<Post>()
                .select(Post::getId, Post::getUserId) // 使用方法引用指定字段
                .eq(Post::getId, postId)
        );
        iUserService.update(new UpdateWrapper<User>()
                .eq("id", post.getUserId())
                .setSql("posts_count = posts_count - 1"));

        // 5.删除es中的帖子信息
        iPostDocService.delPostDocById(String.valueOf(postId));

        // 6.删除该帖子下的所有评论
        QueryWrapper<Comment> commentQw = new QueryWrapper<>();
        commentQw.eq("post_id", postId);
        commentMapper.delete(commentQw);

        // 7.帖子表删除
        this.removeById(postId);

        return Result.success();
    }


    // 修改帖子
    @Transactional
    @Override
    @CacheEvict(value = RedisConstant.POST_RAW_DETAIL_KEY, key = "#updateDTO.id") // 删除缓存
    public Result<String> updatePostById(PostUpdateDTO updateDTO) {
        // 0.确保当前的帖子是属于当前用户的，防止恶意篡改
        Post p = this.getById(updateDTO.getId());
        if (!p.getUserId().equals(updateDTO.getUserId())) {
            return Result.error("您无权对该帖子进行修改");
        }

        Post post = new Post();
        post.setUpdateTime(LocalDateTime.now());
        BeanUtils.copyProperties(updateDTO, post);

        // 1.post--帖子表修改
        this.updateById(post);

        // 2.post_image--帖子图片关联表表修改
        iPostImageService.updatePostImage(post.getId(), updateDTO.getImageUrlList());

        // 3.post_tag--帖子标签关联表修改
        iPostTagService.updatePostTag(post.getId(), updateDTO.getTagIdList());

        // 4.post_topic--帖子话题关联表修改
        iPostTopicService.updatePostTopic(post.getId(), updateDTO.getTopicIdList());

        // 5.es修改
        PostDoc pd = new PostDoc();
        pd.setId(String.valueOf(post.getId()));
        pd.setTitle(post.getTitle());
        pd.setPrivacy(post.getPrivacy());
        pd.setCreateTime(post.getCreateTime());
        iPostDocService.updatePostDocById(pd);

        return Result.success("修改成功");
    }

    // 修改帖子的可见性
    @Override
    @CacheEvict(value = RedisConstant.POST_RAW_DETAIL_KEY, key = "#id") // 删除缓存
    public Result<String> updatePostVisible(Integer id, Integer privacy) {
        UpdateWrapper<Post> uw_post = new UpdateWrapper<>();
        uw_post.eq("id", id).set("privacy", privacy);

        // 1.修改mysql中的帖子表
        this.update(uw_post);

        // 2.修改es中的帖子信息
        PostDoc pd = new PostDoc();
        pd.setId(String.valueOf(id));
        pd.setPrivacy(privacy);
        iPostDocService.updatePostDocById(pd);

        return Result.success("修改成功");
    }


    // 获取所有帖子
    @Override
    public Result<List<Post>> getAllPost() {
        List<Post> list = this.list();
        return Result.success(list);
    }

    // 分页获取所有帖子
    @Override
    public PageVO<Post> getRawPostsByPage (long current, long size) {
        Page<Post> p = new Page<>(current, size);
        p.addOrder(OrderItem.desc("create_time"));   // 按时间降序排列，最新的排在最前

        Page<Post> page_MP = this.page(p); // 使用mp自带的方法，获取分页数据
        return PageVO.of(page_MP); // 将分页数据转为自定义的vo
    }

    // 分页查全部（返回帖子列表展示所需的所有属性）
    @Override
    public PageVO<PostVO_list> getDisplayPostsListByPage(long current, long size) {
        Page<Post> p = new Page<>(current, size, false); // 第三个参数传 false，表示不统计总数
        p.addOrder(OrderItem.desc("create_time"));   // 按时间降序排列，最新的排在最前

        // 1.【查询】原始的帖子信息
        Page<Post> page_MP = this.page(p);

        // 2.提取出获取到的原始帖子信息
        List<Post> records = page_MP.getRecords();
        if (records.isEmpty()) {
            return new PageVO<>(page_MP.getTotal(), page_MP.getPages(), Collections.emptyList());
        }

        // 3.封装PostVO_list
        List<PostVO_list> vo = wrapPostVO_list(records);

        // 4.返回数据
        return new PageVO<>(page_MP.getTotal(), page_MP.getPages(), vo);
    }

    // 获取热门帖子
    @Override
    public List<Post> getHotPosts(int num) {
        // 1.从redis中获取热门帖子的id列表
        String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); // 获取当前日期
        String redisKey = CommonUtils.formatWithStringFormat(RedisConstant.POST_HOT_KEY, today); // 获取redis中的键

        // 查询是否存在这个键，如果不存在，则手动从mysql中查询数据，并缓存进redis
        if (!redisTemplate_String.hasKey(redisKey)) {
            this.refreshDailyHotPosts(today);
        }
        // 键存在，则从redis中获取数据（确保key中数据不能太多，目前限制为1000条），但只获取 num 条，并且顺序不固定
        Set<String> hotPostIds = redisTemplate_String.opsForZSet().range(redisKey, 0, -1);
        if (hotPostIds == null || hotPostIds.isEmpty()) {
            return Collections.emptyList();
        }
        // 打乱顺序，并随机取 num 条
        List<String> shuffledHotPostIds = new ArrayList<>(hotPostIds);
        Collections.shuffle(shuffledHotPostIds);
        List<String> randomHotPostIds = shuffledHotPostIds.subList(0, Math.min(num, shuffledHotPostIds.size()));

        // 2.从mysql中查询信息
        List<Post> hotPosts = lambdaQuery().in(Post::getId, randomHotPostIds).list();
        if(hotPosts.isEmpty()){
            return Collections.emptyList();
        }
        // 打乱顺序
        Collections.shuffle(hotPosts);
        return hotPosts;
    }


    // 分页查，首页推送
    @Override
    public List<PostVO_list> feedForHomePage(Integer userId, int current, int size) {
        List<PostVO_list> voList;

        // 0.如果用户未登录，返回热门帖子
        if (userId == null) {
            voList = wrapPostVO_list(getHotPosts(size));
            return voList;
        }

        // 1. 从Redis获取用户偏好话题和标签
        String topicKey = RedisConstant.USER_PREFERENCE_TOPIC_KEY + userId;
        String tagKey = RedisConstant.USER_PREFERENCE_TAG_KEY + userId;
        
        // 1.1.获取用户最感兴趣的 前10个话题ID
        Set<String> preferredTopicIds = redisTemplate_String.opsForZSet().reverseRange(topicKey, 0, 9);
        
        // 1.2.获取用户最感兴趣的 前10个标签ID
        Set<String> preferredTagIds = redisTemplate_String.opsForZSet().reverseRange(tagKey, 0, 9);

        // 2. 根据用户偏好查询帖子
        Set<Post> recommendedPosts = new HashSet<>();

        // 2.1. 根据偏好标签，推送帖子
        if (preferredTagIds != null && !preferredTagIds.isEmpty()) {
            log.debug("根据标签偏好，推送帖子");
            List<Integer> tagIdList = preferredTagIds.stream().map(Integer::valueOf).collect(Collectors.toList());

            int tagPostsCount = postMapper.getTagPostsCount(tagIdList); // 查询指定标签的帖子数目
            int pageNum = generateWeightedRandomPageNum(tagPostsCount, size); // 随机获取页码，越小的页码（代表帖子越新），概率越高
            Page<Post> p_param_tag = new Page<>(pageNum, size,false); // 构建分页参数

            // 查询含有指定标签的帖子
            List<Post> tagPosts = postMapper.getPostsListByTagIds(p_param_tag, tagIdList);
            recommendedPosts.addAll(tagPosts);
        }

        // 2.2. 根据偏好话题，推送帖子
        if (preferredTopicIds != null && !preferredTopicIds.isEmpty()) {
            log.debug("根据话题偏好，推送帖子");
            List<Integer> topicIdList = preferredTopicIds.stream().map(Integer::valueOf).collect(Collectors.toList());

            int topicPostsCount = postMapper.getTopicPostsCount(topicIdList); // 查询指定话题的帖子数目
            int pageNum = generateWeightedRandomPageNum(topicPostsCount, size); // 随机获取页码，越小的页码（代表帖子越新），概率越高
            Page<Post> p_param_topic = new Page<>(pageNum, size,false); // 构建分页参数
            
            //  查询含有指定话题的帖子
            List<Post> topicPosts = postMapper.getPostsByTopicIds(p_param_topic, topicIdList);
            recommendedPosts.addAll(topicPosts);
        }
        
        // 3. 如果偏好内容不足，一般是新用户，或者帖子数不够（数据量大的情况下一般不会出现），补充热门帖子
        if (recommendedPosts.size() < size) {
            // 计算还需要补充的帖子数量
            int needMore = size - recommendedPosts.size();
            log.debug("偏好内容不足，距离推送目标 {} 个还差 {} 个 ， 从热门榜补充帖子", size, needMore);
            
            // 获取当前已推荐帖子的ID，用于排除
            Set<Integer> existingPostIds = recommendedPosts.stream().map(Post::getId).collect(Collectors.toSet());
            
            // 查询热门帖子
            List<Post> hotPosts = this.getHotPosts(needMore * 2);
            
            // 过滤掉已经在推荐列表中的帖子
            List<Post> newHotPosts = hotPosts.stream()
                    .filter(post -> !existingPostIds.contains(post.getId()))
                    .limit(needMore)
                    .toList();
            
            recommendedPosts.addAll(newHotPosts);
        }

        // 4. 打乱顺序，并限制最终返回数量
        List<Post> postList = new ArrayList<>(recommendedPosts);
        Collections.shuffle(postList);

        if (postList.size() > size) {
            postList = postList.subList(0, size);
        }

        // 5. 封装结果
        log.debug("开始封装结果");
        voList = wrapPostVO_list(postList);
        return voList;
    }

    // 分页查全部，查询我关注的用户发的帖子
    @Override
    public PageVO<PostVO_list> getDisplayPostsListByMyFollowing(Integer userId, long current, long size) {
        Page<Post> p = new Page<>(current, size);
        p.addOrder(OrderItem.desc("create_time"));

        // 1.【查询】原始的帖子信息
        IPage<Post> page_MP = postMapper.listByMyFollowing(userId, p);

        // 2.提取出获取到的原始帖子信息
        List<Post> records = page_MP.getRecords();
        if (records.isEmpty()) {
            return new PageVO<>(page_MP.getTotal(), page_MP.getPages(), Collections.emptyList());
        }

        // 3.封装PostVO_list
        List<PostVO_list> vo = wrapPostVO_list(records);

        // 4.返回数据
        return new PageVO<>(page_MP.getTotal(), page_MP.getPages(), vo);
    }

    // 条件查询,根据标签查找帖子（返回列表集合）
    @Override
    public PageVO<PostVO_list> getPostsListByTagIds_exact(List<Integer> tagIdList, long current, long size) {
        // 1.根据标签查找帖子（精确查找）
        Page<Post> p = new Page<>(current, size);
        IPage<Post> postList = postMapper.getPostsListByTagIds_exact(p, tagIdList, tagIdList.size());
        if (postList.getRecords().isEmpty()) {
            return new PageVO<>(postList.getTotal(), postList.getPages(), Collections.emptyList());
        }

        // 2.封装PostVO_list
        PageVO<PostVO_list> vos = new PageVO<>();
        vos.setTotal(postList.getTotal());
        vos.setPages(postList.getPages());
        vos.setList(wrapPostVO_list(postList.getRecords()));

        // 3.返回数据
        return vos;
    }

    // 条件查询帖子
    @Override
    public PageVO<Post> getInCondition(PostDTO postDTO) {

        // 1.创建实体类对象
        Post post = new Post();
        BeanUtils.copyProperties(postDTO, post);

        // 2.创建分页对象
        Page<Post> pageInstance = new Page<>(postDTO.getCurrent(), postDTO.getSize());

        // 3.创建mybatisPlus分页对象
        Page<Post> page_MP = lambdaQuery()
                // 3.1 构建查询条件
                .eq(post.getId() != null, Post::getId, post.getId())
                .eq(post.getUserId() != null, Post::getUserId, post.getUserId())
                .like(post.getTitle() != null, Post::getTitle, post.getTitle())
                .eq(post.getPrivacy() != null, Post::getPrivacy, post.getPrivacy())
                .like(post.getLocation() != null, Post::getLocation, post.getLocation())

                .page(pageInstance); // 3.2 应用分页

        // 4. 返回自定义的分页对象
        return PageVO.of(page_MP);

    }

    // 根据id查询帖子（返回一个帖子展示所需所有属性）
    @Override
    public PostVO_show getPostInfoById(Integer id, Integer userId){
        PostVO_show vo = new PostVO_show();

        // Spring的缓存功能基于AOP代理实现。当直接在同一个类的某个方法A内部调用另一个带有@Cacheable注解的方法B时，实际上绕过了代理对象，导致缓存注解无法生效
        // 所以要想缓存注解生效，必须使用代理对象
        PostServiceImpl proxy = (PostServiceImpl) AopContext.currentProxy();
        PostVO_raw rawData = proxy.getPostById_raw(id);
        if(rawData == null){
            return null;
        }

        // 为vo的属性赋值
        BeanUtils.copyProperties(rawData, vo);
        if(userId != null){
            //判断当前用户是否已经点赞
            Double score_like = getScore(RedisConstant.USER_LIKE_POST_KEY + userId, String.valueOf(vo.getId()));
            vo.setLiked(score_like != null);

            // 判断当前用户是否已经收藏
            Double score_collect = getScore(RedisConstant.USER_COLLECT_POST_KEY + userId, String.valueOf(vo.getId()));
            vo.setCollected(score_collect != null);
        }

        return vo;
    }

    /**根据标题查询帖子，从es中查询
     * @param title 用户输入的标题内容
     * @param current  当前页码
     * @param size 每页显示条数
     * @return PageVO<PostVO_list>
     */
    @Override
    public PageVO<PostVO_list> getByTitle(String title, long current, long size) {
        // 1.从es中查询帖子信息
        PageVO<PostDoc> pd_vo = iPostDocService.getPostDocByTitle(title, current, size);
        if(pd_vo.getList().isEmpty()){
            return new PageVO<>(pd_vo.getTotal(), pd_vo.getPages(), Collections.emptyList());
        }

        // 2.封装 -- PostDoc -->  Post
        List<Post> postList = wrapPost(pd_vo.getList());

        // 3.封装 -- Post --> PostVO_list
        List<PostVO_list> voLists = wrapPostVO_list(postList);

        // 4.返回
        PageVO<PostVO_list> vo = new PageVO<>();
        vo.setTotal(pd_vo.getTotal());
        vo.setPages(pd_vo.getPages());
        vo.setList(voLists);
        return vo;
    }

    // 根据用户id，返回用户创作的帖子信息
    @Override
    public PageVO<PostVO_list> getByUserId(Integer userId, long current, long size) {
        // 一、查询 -- 查数据库表
        // 二、组装 -- 组装要返回的数据。本方法主要是组装PostVO_list，在setImageUrlList()上做了许多功夫
        // 三、返回 -- 返回数据


        // 1.【查询】原始表
        Page<Post> page_MP_res = lambdaQuery()
                .eq(Post::getUserId, userId)
                .orderByDesc(Post::getCreateTime)
                .page(new Page<>(current, size));

        // 2.获取所有帖子ID
        List<Integer> postIdList = page_MP_res.getRecords().stream().map(Post::getId).toList();
        PageVO<PostVO_list> vo = new PageVO<>();
        if(postIdList.isEmpty()){
            return vo; // 该用户没有创作的帖子，返回空vo对象
        }

        // 3.批量查询每个帖子的【第一张】图片，使用map集合存储信息
        // 3.1.【查询】出所有的图片信息
        List<PostImage> pImgList = iPostImageService.list(new QueryWrapper<PostImage>()
                .in("post_id", postIdList)
        );

        // 3.2. 收集封面
        // key：帖子ID， value：帖子对应的第一张图片的URL
        Map<Integer, String> firstImageMap = pImgList.stream()
                .collect(Collectors.groupingBy(
                        PostImage::getPostId,
                        // 对每个分组内的图片按sort_order升序排序，取第一个（sort_order的值最小，才能当封面）
                        Collectors.collectingAndThen(
                                Collectors.toList(), // 将属于同一个 postId分组的所有 PostImage对象先收集到一个 List<PostImage>中
                                list -> list.stream()
                                        .min(Comparator.comparingInt(PostImage::getSortOrder))
                                        .map(PostImage::getImageUrl)
                                        .orElse(null)
                        )
                ));

        // 4.转成vo并返回
        List<PostVO_list> pvoL_list = new ArrayList<>();
        page_MP_res.getRecords().forEach(post -> {
            PostVO_list pvoL = new PostVO_list();
            BeanUtils.copyProperties(post, pvoL);

            // 设置图片列表（不用全获取，用来展示列表只用返回第一张就行了，相当于封面）
            String firstImage = firstImageMap.getOrDefault(post.getId(), null);
            pvoL.setImageUrlList(firstImage != null ? List.of(firstImage) : List.of());

            pvoL_list.add(pvoL);
        });
        vo.setTotal(page_MP_res.getTotal());
        vo.setPages(page_MP_res.getPages());
        vo.setList(pvoL_list);
        return vo;
    }

    // 根据用户id，返回用户点赞的帖子信息
    @Override
    public PageVO<PostVO_list> getByUserLike(Integer userId, long current, long size) {
        long start = (current - 1) * size; // 起始下标
        long end = start + size - 1; // 结束下标

        // 1.从redis中查询原始数据
        Set<String> ids = redisTemplate_String.opsForZSet().reverseRange(RedisConstant.USER_LIKE_POST_KEY + userId, start, end);
        if(ids == null ||ids.isEmpty()){
            return new PageVO<>(0L, 0L, Collections.emptyList());
        }
        Page<Post> page_MP_res = lambdaQuery()
                .in(Post::getId, ids)
                .orderByDesc(Post::getCreateTime)
                .page(new Page<>(current, size));
        if(page_MP_res.getTotal() == 0){
            return PageVO.empty(page_MP_res);
        }

        // 2.组装数据 -- 将 Post 封装成 PostVO_list
        List<PostVO_list> vo_records = wrapPostVO_list(page_MP_res.getRecords());

        // 3.返回数据
        PageVO<PostVO_list> vo = new PageVO<>();
        vo.setTotal(page_MP_res.getTotal());
        vo.setPages(page_MP_res.getPages());
        vo.setList(vo_records);
        return vo;
    }

    // 根据用户id，返回用户收藏的帖子信息
    @Override
    public PageVO<PostVO_list> getByUserCollect(Integer userId, long current, long size) {
        long start = (current - 1) * size; // 起始下标
        long end = start + size - 1; // 结束下标

        // 1.从redis中查询原始数据
        Set<String> ids = redisTemplate_String.opsForZSet().reverseRange(RedisConstant.USER_COLLECT_POST_KEY + userId, start, end);
        if(ids == null ||ids.isEmpty()){
            return new PageVO<>(0L, 0L, Collections.emptyList());
        }
        Page<Post> page_MP_res = lambdaQuery()
                .in(Post::getId, ids)
                .orderByDesc(Post::getCreateTime)
                .page(new Page<>(current, size));
        if(page_MP_res.getTotal() == 0){
            return PageVO.empty(page_MP_res);
        }

        // 2.组装数据 -- 将 Post 封装成 PostVO_list
        List<PostVO_list> vo_records = wrapPostVO_list(page_MP_res.getRecords());

        // 3.返回数据
        PageVO<PostVO_list> vo = new PageVO<>();
        vo.setTotal(page_MP_res.getTotal());
        vo.setPages(page_MP_res.getPages());
        vo.setList(vo_records);
        return vo;
    }



    /**
     * redis相关
     */
    // 浏览帖子，记录用户阅读帖子的标签与话题
    @Override
    public void read(PostReadDTO prDTO) {
        // 1.获取用户id
        Integer userId = BaseContext.getCurrentId();
        if(userId == null){
            return; // 用户未登录
        }
        if(prDTO.getPostId() == null){
            return; // 帖子id为空
        }

        // 2.操作redis，记录数据
        // 2.1.获取key
        String postKey = RedisConstant.USER_CURRENT_POST_KEY + userId;
        String tagKey = RedisConstant.USER_PREFERENCE_TAG_KEY + userId;
        String topicKey = RedisConstant.USER_PREFERENCE_TOPIC_KEY + userId;

        // 2.2.绑定ops，获取redis操作对象
        BoundZSetOperations<String, String> ops_post = redisTemplate_String.boundZSetOps(postKey);
        BoundZSetOperations<String, String> ops_tag = redisTemplate_String.boundZSetOps(tagKey);
        BoundZSetOperations<String, String> ops_topic = redisTemplate_String.boundZSetOps(topicKey);

        // 2.3.添加阅读数据记录
        long currentTime = System.currentTimeMillis();
        // 2.3.1. 帖子
        ops_post.add(prDTO.getPostId().toString(), currentTime);
        ops_post.expire(Duration.ofDays(10)); // 过期时间为10天
        ops_post.removeRange(0, -(RedisConstant.USER_CURRENT_READ_POST_MAXNUM + 1)); // 只保留阅读时间最靠前的100条数据

        // 2.3.2. 标签
        if (prDTO.getTagIdList() != null && !prDTO.getTagIdList().isEmpty()) {
            Set<ZSetOperations.TypedTuple<String>> tagTuples = new HashSet<>();
            for (Integer tagId : prDTO.getTagIdList()) {
                tagTuples.add(new DefaultTypedTuple<>(tagId.toString(), (double)currentTime)); // 为元组赋值
            }
            ops_tag.add(tagTuples); // 一次性添加
        }
        ops_tag.removeRange(0, -(RedisConstant.USER_PREFERENCE_TAG_MAXNUM + 1)); // 只保留阅读时间最靠前的15条数据


        // 2.3.3. 话题
        if (prDTO.getTopicIdList() != null && !prDTO.getTopicIdList().isEmpty()) {
            Set<ZSetOperations.TypedTuple<String>> topicTuples = new HashSet<>();
            for (Integer topicId : prDTO.getTopicIdList()) {
                topicTuples.add(new DefaultTypedTuple<>(topicId.toString(), (double)currentTime)); // 为元组赋值
            }
            ops_topic.add(topicTuples); // 一次性添加
        }
        ops_topic.removeRange(0, -(RedisConstant.USER_PREFERENCE_TOPIC_MAXNUM + 1)); // 只保留阅读时间最靠前的10条数据
    }

    // 点赞帖子
    @Override
    public void like(Integer postId) {
        // 1.获取用户id
        Integer userId = BaseContext.getCurrentId();
        if(userId == null){
            return; // 用户未登录
        }

        String key = RedisConstant.USER_LIKE_POST_KEY + userId;
        String value = postId.toString();
        long timestamp = System.currentTimeMillis();

        // 2.判断当前用户是否已经点过赞
        Double score = getScore(key, value);

        // 2.1.如果未点赞，则可以点赞
        if(score == null){
            boolean isSuccess = update()
                    .setSql("likes_count = likes_count + 1") // 数据库点赞数+1
                    .eq("id", postId)
                    .update();
            if(isSuccess){
                redisTemplate_String.opsForZSet().add(key, value, timestamp); //保存记录到redis的zSet集合中(key, value, score)
            }
        }
        // 2.2.如果已经点赞，则取消点赞
        else {
            boolean isSuccess = update()
                    .setSql("likes_count = likes_count - 1") // 数据库点赞数-1
                    .eq("id", postId)
                    .update();
            if(isSuccess){
                redisTemplate_String.opsForZSet().remove(key, value); //把记录从redis的zSet集合中移除
            }
        }

        // 3.确保每个用户最多记录100条点赞
        long count = redisTemplate_String.opsForZSet().zCard(key);
        if(count > MAX_LIKE_COUNT){
            // 若有104个记录，只有删4个元素的情况下，才会还剩100条记录。
            // 故第三个参数end是 104-(100+1)=3 ，因为从0索引开始删，0,1,2,3，刚好4个
            redisTemplate_String.opsForZSet().removeRange(
                    key,
                    0,
                    count - (MAX_LIKE_COUNT + 1)
            );
        }
    }

    // 收藏帖子
    @Override
    public void collect(Integer postId) {
        // 1.获取用户id
        Integer userId = BaseContext.getCurrentId();
        if(userId == null){
            return; // 用户未登录
        }

        String key = RedisConstant.USER_COLLECT_POST_KEY + userId;
        String value = postId.toString();
        long timestamp = System.currentTimeMillis();

        // 2.判断当前用户是否已经收藏过
        Double score = getScore(key, value);

        // 2.1.如果未收藏，则可以收藏
        if(score == null){
            boolean isSuccess = update()
                    .setSql("favorite_count = favorite_count + 1") // 数据库收藏数+1
                    .eq("id", postId)
                    .update();
            if(isSuccess){
                redisTemplate_String.opsForZSet().add(key, value, timestamp); //保存记录到redis的zSet集合中(key, value, score)
            }
        }
        // 2.2.如果已经收藏，则取消收藏
        else {
            boolean isSuccess = update()
                    .setSql("favorite_count = favorite_count - 1") // 数据库收藏数-1
                    .eq("id", postId)
                    .update();
            if(isSuccess){
                redisTemplate_String.opsForZSet().remove(key, value); //把记录从redis的zSet集合中移除
            }
        }

        // 3.确保每个用户最多记录500条收藏
        long count = redisTemplate_String.opsForZSet().zCard(key);
        if(count > 500){
            // 若有540个记录，只有删40个元素的情况下，才会还剩500条记录。
            // 故第三个参数end是 540-(500+1)=39 ，因为从 0 索引开始删，0,1,2,3...39，刚好40个
            redisTemplate_String.opsForZSet().removeRange(
                    key,
                    0,
                    count - (MAX_COLLECT_COUNT + 1)
            );
        }
    }



    // 封装PostVO_list (Post --> PostVO_list)
    public List<PostVO_list> wrapPostVO_list(List<Post> postList) {

        // 1.提取帖子的Id集合
        List<Integer> postIdList = postList.stream().map(Post::getId).toList();

        // 2.批量【查询】用户信息
        Set<Integer> userIds = postList.stream().map(Post::getUserId).collect(Collectors.toSet());
        // 只查询需要的字段：id和username
        Map<Integer, User> userMap = iUserService.lambdaQuery()
                .select(User::getId, User::getUsername, User::getAvatarUrl)  // 仅选择需要的字段
                .in(User::getId, userIds)                // 条件：id在用户ID集合中
                .list()                                  // 执行查询
                .stream()
                .collect(Collectors.toMap(User::getId, Function.identity()));

        // 3.批量【查询】图片URL
        QueryWrapper<PostImage> qw_pImg = new QueryWrapper<PostImage>().in("post_id", postIdList).orderByAsc("sort_order");
        Map<Integer, List<String>> imageMap = iPostImageService.list(qw_pImg).stream()
                .collect(Collectors.groupingBy(
                        PostImage::getPostId,
                        Collectors.mapping(PostImage::getImageUrl, Collectors.toList())
                ));

        // 4.转换为VO
        return postList.stream().map(post -> {
            PostVO_list vo = new PostVO_list()
                    .setId(post.getId())
                    .setUserId(post.getUserId())
                    .setTitle(post.getTitle())
                    .setPrivacy(post.getPrivacy())
                    .setLocation(post.getLocation())
                    .setLikesCount(post.getLikesCount())
                    .setCommentsCount(post.getCommentsCount())
                    .setFavoriteCount(post.getFavoriteCount())
                    .setSharesCount(post.getSharesCount())
                    .setViewsCount(post.getViewsCount())
                    .setHotScore(post.getHotScore())
                    .setStatus(post.getStatus())
                    .setCreateTime(post.getCreateTime())
                    .setUpdateTime(post.getUpdateTime())
                    .setImageUrlList(imageMap.getOrDefault(post.getId(), Collections.emptyList()));

            if(userMap.get(post.getUserId()) != null){
                vo.setUserAvatarUrl(userMap.get(post.getUserId()).getAvatarUrl());
                vo.setUsername(userMap.get(post.getUserId()).getUsername());
            }else {
                vo.setUsername("用户已注销");
            }
            return vo;
        }).toList();
    }

    // 封装PostList (PostDoc -->  Post)
    public List<Post> wrapPost(List<PostDoc> postDocList) {
        List<Post> postList = new ArrayList<>();

        // 1. 收集所有需要查询的帖子ID
        List<String> ids = postDocList.stream().map(PostDoc::getId).toList();

        // 2. 批量查询帖子并创建map映射
        Map<Integer, Post> postMap = this.listByIds(ids).stream() // 只执行一次查询即可
                .collect(Collectors.toMap(Post::getId, Function.identity()));

        // 3. 组装结果列表
        for (PostDoc postDoc : postDocList) {
            postList.add(postMap.get(Integer.valueOf(postDoc.getId())));
        }
        return postList;
    }

    // 根据id查询帖子（返回在数据库层面，多表联查后的帖子信息）
    @Cacheable(value = RedisConstant.POST_RAW_DETAIL_KEY, key = "#id") // 将查询结果缓存
    public PostVO_raw getPostById_raw(Integer id) {
        Post post = this.getById(id);
        if(post == null){
            return null;
        }
        PostVO_raw pvo_raw = new PostVO_raw();

        BeanUtils.copyProperties(post, pvo_raw);

        UserVO_post userInfo = iUserService.getById_PostQuery(post.getUserId());
        pvo_raw.setUserInfo(userInfo);

        List<String> postImgUrlList = iPostImageService.list(new QueryWrapper<PostImage>()
                .eq("post_id", post.getId())
        ).stream().map(PostImage::getImageUrl).toList();
        pvo_raw.setImageUrlList(postImgUrlList);

        List<Integer> postTagIdList = iPostTagService.list(new QueryWrapper<PostTag>()
                .eq("post_id", post.getId())
        ).stream().map(PostTag::getTagId).toList();
        pvo_raw.setTagIdList(postTagIdList);

        List<Integer> postTopicIdList = iPostTopicService.list(new QueryWrapper<PostTopic>()
                .eq("post_id", post.getId())
        ).stream().map(PostTopic::getTopicId).toList();
        pvo_raw.setTopicIdList(postTopicIdList);

        return pvo_raw;
    }

    // 骗编译器，让它不要报警告
    public Double getScore(String key, String value) {
        return redisTemplate_String.opsForZSet().score(key, value);
    }


    /**
     * 生成加权随机页码，越小的页码概率越高
     * 使用指数衰减的概率分布，页码0的概率最高，随着页码增加概率指数递减
     * @param totalPosts 总帖子数量
     * @param pageSize 每页帖子数量
     * @return 随机生成的页码
     */
    private int generateWeightedRandomPageNum(int totalPosts, int pageSize) {
        if (totalPosts <= 0 || pageSize <= 0) {
            return 0;
        }
        int maxPageNum = Math.max(0, (totalPosts - 1) / pageSize); // 计算最大页码
        
        // 如果只有一页或没有数据，直接返回0
        if (maxPageNum == 0) {
            return 0;
        }
        
        // 使用指数衰减分布，lambda值越大，衰减越快（页码0的概率越高）
        double lambda = 0.6; // 衰减系数，可以调整这个值来改变分布形状
        
        // 生成随机数
        Random random = new Random();
        double randomValue = random.nextDouble();
        
        // 使用指数分布的逆函数生成页码
        // P(X <= x) = 1 - e^(-lambda * x)
        // 逆函数：x = -ln(1 - randomValue) / lambda
        double exponentialValue = -Math.log(1 - randomValue) / lambda;
        
        // 将连续值映射到离散页码，并限制在有效范围内
        return Math.min((int) exponentialValue, maxPageNum);
    }



    // 从mysql查询热门帖子
    private List<Post> calculateHotPostsForCache(int num) {
        LocalDateTime now = LocalDateTime.now();

        // 1. 随机生成时间窗口
        Random random_time = new Random();
        int timeLevel0 = 1;
        int timeLevel1 = 7 + random_time.nextInt(7);
        int timeLevel2 = 30 + random_time.nextInt(30);
        int timeLevel3 = 90 + random_time.nextInt(90);
        int timeLevel4 = 365 + random_time.nextInt(90);
        int[] timeWindows = {timeLevel0, timeLevel1, timeLevel2, timeLevel3, timeLevel4}; // 7天、30天、90天、1年的时间窗口（天）

        // 2. 随机生成权重
        Random random = new Random();
        double likesWeight = 0.4 + random.nextDouble();
        double commentsWeight = 0.2 + random.nextDouble();
        double favoriteWeight = 0.1 + random.nextDouble();
        double timeWeight = 0.3 + random.nextDouble();

        int queryLimit = num;

        Set<Integer> existingIds = new HashSet<>(); // 用于跟踪已添加的帖子ID
        List<Post> resultPosts = new ArrayList<>(); // 用于存储最终结果

        // 3.遍历时间窗口获取数据
        for (int days : timeWindows) {
            LocalDateTime startTime = now.minusDays(days);

            // 3.1.先查一次数量
            long count = lambdaQuery()
                    .ge(Post::getCreateTime, startTime)
                    .eq(Post::getStatus, 2)  // 只查询正常状态的帖子
                    .eq(Post::getPrivacy, 1)  // 只查询公开的帖子
                    .count();
            if(count < 1){
                continue;
            }
            long maxPageNum = count / queryLimit + 1;
            int randomPageNum = random.nextInt((int) maxPageNum); // 随机选择一个页数，更具有偶然性

            // 3.2.再正经查一次(但只查id)，分好页，给下一个查询
            List<Post> PostList_pagePrePare = lambdaQuery()
                    .select(Post::getId)
                    .ge(Post::getCreateTime, startTime)
                    .eq(Post::getStatus, 2)  // 只查询正常状态的帖子
                    .eq(Post::getPrivacy, 1)  // 只查询公开的帖子
                    .notIn(!existingIds.isEmpty(), Post::getId, existingIds)
                    .page(new Page<>(randomPageNum, queryLimit, false))
                    .getRecords();
            List<Integer> idList_pp = PostList_pagePrePare.stream().map(Post::getId).toList();
            if (idList_pp.isEmpty()){
                continue;
            }

            // 3.3.根据id查，将帖子排序
            List<Post> candidatePosts = lambdaQuery()
                    .in(Post::getId, idList_pp)
                    .last("ORDER BY (LOG10(likes_count + 1) * " + likesWeight  + " + " +
                            "LOG10(comments_count + 1) * " + commentsWeight  + " + " +
                            "LOG10(favorite_count + 1) * " + favoriteWeight  + " + " +
                            "EXP(-TIMESTAMPDIFF(HOUR, create_time, NOW()) / 168.0) * " + (timeWeight  * 10) + ") DESC " +
                            "LIMIT " + queryLimit)
                    .list();

            // 3.4.这段时间内没有帖子就下一个时间点查找，不走下面的逻辑
            if (!candidatePosts.isEmpty()) {
                // 过滤掉已添加的重复帖子，只添加新的帖子
                List<Post> newPosts = candidatePosts.stream().filter(post -> !existingIds.contains(post.getId())).toList();

                if (!newPosts.isEmpty()) {
                    resultPosts.addAll(newPosts);
                    newPosts.forEach(post -> existingIds.add(post.getId())); // 更新已添加的帖子ID集合

                    // 如果已经获取到足够的帖子，提前退出
                    if (resultPosts.size() >= num) {
                        break;
                    }
                    queryLimit = num - resultPosts.size(); // 动态调整下一次查询的数量（避免重复查询已获取的数据）
                }
            }
        }

        return resultPosts;
    }

    // 计算帖子热度分数
    private double calculateHotScore(Post post) {
        // 计算热度分数：可以用归一化后的综合分数
        // 或者用时间戳+热度，确保新帖子有一定优势
        double baseScore = Math.log10(post.getLikesCount() + 1) * 0.3 +
                Math.log10(post.getCommentsCount() + 1) * 0.2 +
                Math.log10(post.getFavoriteCount() + 1) * 0.1 +
                1.0; // 增加一个全局基础分，确保分数恒大于0;

        // 时间衰减：越新的帖子衰减越小
        long hoursOld = ChronoUnit.HOURS.between(post.getCreateTime(), LocalDateTime.now());

        // 衰减因子：24小时内的帖子几乎不衰减，超过24小时开始衰减
        double timeDecay = Math.exp(-hoursOld / 168.0); // 168小时=1周

        // 最终分数 = 基础分数 * 时间衰减系数
        // 这样新帖子有优势，但不会完全压制老的高质量内容
        return baseScore * (0.5 + 0.5 * timeDecay);
    }

    // 刷新指定日期的热门帖子
    @Override
    public void refreshDailyHotPosts(String date) {
        String redisKey = CommonUtils.formatWithStringFormat(RedisConstant.POST_HOT_KEY, date);

        // 1. 获取热门帖子
        List<Post> hotPosts = this.calculateHotPostsForCache(POST_HOT_NUM); // 计算1000条

        // 2. 清空原有数据
        redisTemplate_String.delete(redisKey);

        // 3. 写入Redis
        if (!hotPosts.isEmpty()) {
            Set<ZSetOperations.TypedTuple<String>> tuples = new HashSet<>();
            for (Post post : hotPosts) {
                double score = calculateHotScore(post); // 分数是热度值
                tuples.add(ZSetOperations.TypedTuple.of(post.getId().toString(), score));
            }

            redisTemplate_String.opsForZSet().add(redisKey, tuples); // 添加数据
            redisTemplate_String.expire(redisKey, Duration.ofDays(7)); // 设置过期时间（7天）
        }
        log.info("日期 {} 热门帖子榜已刷新，共 {} 条", date, hotPosts.size());
    }

}
